/*
 * Copyright (c) 1997, 1998, 2003, 2006 Aleksandar Samardzic
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <assert.h>		/* for assert() function */
#include <stdlib.h>		/* for malloc() function */
#include <string.h>		/* for memset() function */
#include "scene.h"
#include "bounding_volume_hierarchy.h"
#include "scene_builder.h"
#include "voxel_grid.h"

/* Scene_Init - Scene class constuctor */
void
Scene_Init(this)
     PScene         *this;
{
	memset(this, 0, sizeof(PScene));

	List_Init(&(this->objects));
	List_Init(&(this->lights));
	List_Init(&(this->materials));
}

/* Scene_Clean - Scene class destructor */
void
Scene_Clean(this)
     PScene         *this;
{
	PPrimitive     *pPrimitive;
	PLight         *pLight;
	PMaterial      *pMaterial;
	PListIterator   listIt;

	/* clean objects */
	for (ListIterator_Attach(&listIt, &(this->objects));
	     ListIterator_Next(&listIt, (void **) &pPrimitive);) {
		pPrimitive->Clean(pPrimitive);
		free(pPrimitive);
	}
	List_Clean(&(this->objects));

	/* clean lights */
	for (ListIterator_Attach(&listIt, &(this->lights));
	     ListIterator_Next(&listIt, (void **) &pLight);)
		free(pLight);
	List_Clean(&(this->lights));

	/* clean materials */
	for (ListIterator_Attach(&listIt, &(this->materials));
	     ListIterator_Next(&listIt, (void **) &pMaterial);)
		free(pMaterial);
	List_Clean(&(this->materials));

	/* clean data access structure */
	this->pDAS->Clean(this->pDAS);
	free(this->pDAS);
}

/* Scene_Serialize - read scene from standard input */
void
Scene_Serialize(this, pSlabs, maxRayDepth, useGrid, seed)
     PScene         *this;
     PSlabs         *pSlabs;
     Byte            maxRayDepth;
     Bool            useGrid;
     Word            seed;
{
	SCENEBUILDER_DECLARE(sceneBuilder)
	NFFReader       nffReader;

	/* ask reader to use builder in order to build scene */
	SceneBuilder_Init(&sceneBuilder, this);
	NFFReader_Init(&nffReader, (NFFBuilder *) & sceneBuilder);
	NFFReader_ParseNFF(&nffReader);

	/* set maximal ray depth allowable */
	this->maxRayDepth = maxRayDepth;

	/* create appropriate data access structure */
	if (useGrid) {
		this->pDAS = malloc(sizeof(PVoxelGrid));
		assert(this->pDAS != NULL);
		VOXELGRID_DECLAREDYNAMIC(*((PVoxelGrid *) this->pDAS))
		    VoxelGrid_Init((PVoxelGrid *) this->pDAS,
				   &(this->objects), pSlabs);
	} else {
		this->pDAS = malloc(sizeof(PBoundingVolumeHierarchy));
		assert(this->pDAS != NULL);
		BOUNDINGVOLUMEHIERARCHY_DECLAREDYNAMIC(*
						       ((PBoundingVolumeHierarchy *) this->pDAS))
		    BoundingVolumeHierarchy_Init((PBoundingVolumeHierarchy
						  *) this->pDAS, pSlabs,
						 &(this->objects), seed);
	}
}

/* Scene_Render - render scene into rectangle of pixels */
void
Scene_Render(this, pPixels, pRectangle)
     PScene         *this;
     PColor         *pPixels;
     PRectangle     *pRectangle;
{
	PViewingParameters *pViewingParameters;
	double         *PRP,
	               *VPN,
	               *VUP,
	               *VRP;
	double          umin,
	                vmin,
	                umax,
	                vmax;
	PVector         u,
	                v;
	double          mul,
	                den;
	PVector         delta_u,
	                delta_v;
	PVector         focus;
	DWord           row,
	                col;
	PColor         *pPixel;
	PRay            ray;

	/* calculate u and v directions */
	pViewingParameters = &(this->viewingParameters);
	PRP = pViewingParameters->PRP, VPN = pViewingParameters->VPN, VUP =
	    pViewingParameters->VUP, VRP = pViewingParameters->VRP;
	Vector_CrossProduct(u, VPN, VUP);
	Vector_CrossProduct(v, u, VPN);

	/* calculate delta_u and delta_v vectors, as well as u and v
	 * window dimensions */
	mul =
	    (pViewingParameters->umax -
	     pViewingParameters->umin) / this->window.width;
	Vector_Mul(delta_u, u, mul);
	mul =
	    (pViewingParameters->vmax -
	     pViewingParameters->vmin) / this->window.height;
	Vector_Mul(delta_v, v, mul);
	umin =
	    pViewingParameters->umin + (pViewingParameters->umax -
					pViewingParameters->umin) *
	    pRectangle->left / this->window.width;
	umax =
	    pViewingParameters->umin + (pViewingParameters->umax -
					pViewingParameters->umin) *
	    (pRectangle->left + pRectangle->width) / this->window.width;
	vmin =
	    pViewingParameters->vmin + (pViewingParameters->vmax -
					pViewingParameters->vmin) *
	    pRectangle->bottom / this->window.height;
	vmax =
	    pViewingParameters->vmin + (pViewingParameters->vmax -
					pViewingParameters->vmin) *
	    (pRectangle->bottom +
	     pRectangle->height) / this->window.height;

	/* for each pixel in window */
	for (row = pRectangle->bottom;
	     row < pRectangle->bottom + pRectangle->height; row++)
		for (col = pRectangle->left, pPixel =
		     pPixels + (row * (this->window.width + 1) + col);
		     col < pRectangle->left + pRectangle->width; col++) {
			/* set ray starting point */
			Vector_Assign(ray.p0, PRP);

			/* calculate ray direction */
			focus[X] =
			    VRP[X] + umin * u[X] + (col -
						    pRectangle->left) *
			    delta_u[X] + vmin * v[X] + (row -
							pRectangle->
							bottom) *
			    delta_v[X];
			focus[Y] =
			    VRP[Y] + umin * u[Y] + (col -
						    pRectangle->left) *
			    delta_u[Y] + vmin * v[Y] + (row -
							pRectangle->
							bottom) *
			    delta_v[Y];
			focus[Z] =
			    VRP[Z] + umin * u[Z] + (col -
						    pRectangle->left) *
			    delta_u[Z] + vmin * v[Z] + (row -
							pRectangle->
							bottom) *
			    delta_v[Z];
			Vector_Sub(ray.dp, focus, PRP);
			Vector_Normalize(ray.dp, mul, den);

			/* set ray t value and ray depth */
			ray.t = DBL_MAX, ray.depth = 1;

			/* intersect ray with primitives in scene and find 
			 * pixel color */
			Scene_Shade(this, pPixel,
				    Scene_Intersect(this, &ray), &ray);
			pPixel++;
		}
}

/* Scene_Intersect - intersect scene primitives with a ray */
PPrimitive     *
Scene_Intersect(this, pRay)
     PScene         *this;
     PRay           *pRay;
{
	/* forward intersection calculation to data access structure */
	return this->pDAS->Intersect(this->pDAS, pRay);
}

/* Scene_Shade - calculate color in intersection point */
void
Scene_Shade(this, pPixel, pPrimitive, pRay)
     PScene         *this;
     PColor         *pPixel;
     PPrimitive     *pPrimitive;
     PRay           *pRay;
{
	PVector         point;
	PVector         normal;
	double          mul,
	                den;
	PMaterial      *pMaterial;
	Bool            isInside;
	PColor          color;
	int             axis;
	PLight         *pLight;
	PVector         halfway;
	PRay            shadowRay;
	PRay            reflectedRay,
	                refractedRay;
	PVector         V;
	PListIterator   listIt;

	/* if there is no intersection, set color to background color
	 * value */
	if (!pPrimitive) {
		for (axis = RED; axis <= BLUE; axis++)
			(*pPixel)[axis] = this->bckgColor[axis];
		return;
	}

	/* calculate intersection point and primitive normal in this point 
	 */
	Vector_LERP(point, pRay->p0, pRay->dp, pRay->t);
	pPrimitive->CalcNormal(pPrimitive, point, normal);

	/* see if point is inside of primitive and, ir yes, invert normal */
	if (isInside = (Bool) (Vector_DotProduct(pRay->dp, normal) > 0))
		Vector_MulAssign(normal, -1);

	/* remember inverted ray direction for later calculation */
	Vector_Invert(V, pRay->dp);

	/* initialize color with ambient color */
	pMaterial = pPrimitive->pMaterial;
	for (axis = RED; axis <= BLUE; axis++)
		(*pPixel)[axis] = pMaterial->ka * pMaterial->color[axis];

	/* for each light in scene */
	for (ListIterator_Attach(&listIt, &(this->lights));
	     ListIterator_Next(&listIt, (void **) &pLight);) {
		/* create and trace shadow ray */
		Ray_CreateShadowRay(&shadowRay, point, pLight->position);

		mul = Vector_DotProduct(shadowRay.dp, normal);

		if (mul > 0 && !Scene_Intersect(this, &shadowRay)) {
			/* add diffuse component to color */
			if (pMaterial->kd != 0)
				for (axis = RED; axis <= BLUE; axis++)
					(*pPixel)[axis] +=
					    mul * pMaterial->kd *
					    pMaterial->color[axis] *
					    pLight->color[axis];

			/* calculate halfway vector */
			Vector_Halfway(halfway, V, shadowRay.dp);
			Vector_Normalize(halfway, mul, den);

			/* add specular component to color */
			if (pMaterial->ks != 0) {
				mul = Vector_DotProduct(halfway, normal);
				if (pMaterial->kt != 0)
					mul = fabs(mul);
				if (mul > 0) {
					mul = pow(mul, pMaterial->n);
					for (axis = RED; axis <= BLUE;
					     axis++)
						(*pPixel)[axis] +=
						    mul * pMaterial->ks *
						    pMaterial->
						    color[axis] *
						    pLight->color[axis];
				}
			}
		}
	}

	if (pRay->depth < this->maxRayDepth) {
		/* trace reflected ray */
		if (pMaterial->kr != 0
		    && Ray_CreateReflectedRay(&reflectedRay, pRay, point,
					      normal)) {
			Scene_Shade(this, &color,
				    Scene_Intersect(this, &reflectedRay),
				    &reflectedRay);
			for (axis = RED; axis <= BLUE; axis++)
				(*pPixel)[axis] +=
				    pMaterial->kr * color[axis];
		}

		/* trace refracted ray */
		if (pMaterial->kt != 0
		    && Ray_CreateRefractedRay(&refractedRay, pRay, point,
					      normal,
					      isInside ? 1 /
					      pMaterial->ir : pMaterial->
					      ir)) {
			Scene_Shade(this, &color,
				    Scene_Intersect(this, &refractedRay),
				    &refractedRay);
			for (axis = RED; axis <= BLUE; axis++) {
				(*pPixel)[axis] *= 1 - pMaterial->kt;
				(*pPixel)[axis] +=
				    pMaterial->kt * color[axis];
			}
		}
	}

	/* check for color overflow */
	for (axis = RED; axis <= BLUE; axis++)
		if ((*pPixel)[axis] > 1)
			(*pPixel)[axis] = 1;
}
